Oberon/F is decomposed into a hierarchy of modules which provide systematically constructed abstractions. Features which are not used can be ignored. This makes it possible to start with an explanation of the core concepts (like in this section and in the tutorial part of this book), and later reveal further capabilities.
Abstractions in Oberon/F are chosen such that they do not impose undue demands upon the underlying hardware. However, a bitmap display and a mouse are required, i.e. no attempt is made to achieve backward compatibility with character-oriented displays.
At the "bottom" of Oberon/F, the differences between hardware platforms are hidden as much as possible. At the "top", the differences between user-interfaces are hidden as much as possible. For example, there are no assumptions built into Oberon/F about the underlying window system, e.g. whether it provides overlapping or tiling windows. In fact, the abstraction of a window is just as absent from Oberon/F as the abstraction of an application.
Because Oberon/F constitutes a layer between the operating system and the application, it must be efficient. This not only applies to speed, but also to memory and disk requirements. However, the efficiency of a particular Oberon/F implementation is necessarily dependent on the quality of the underlying operating system and hardware.
Oberon/F interfaces are safe, i.e. an Oberon/F application which doesn't use the low-level facilities of module SYSTEM is type-safe. A module which is type-safe may only create local damage, even if it contains arbitrary errors. In particular, it cannot destroy invariants guaranteed by other modules. This bug containment is especially relevant if objects of differing origins are combined in a single document.
A central aspect of Oberon/F is its extensibility. It is possible to extend most major Oberon/F data types, in order to add new functionality to the existing system. Many basic services can be replaced or supplemented by new customized versions, in a way that existing applications can make use of the new facilities. To reach this degree of extensibility and configurability, module interfaces are constructed in a systematic way, which is described in this chapter.
For the design of the Oberon/F interfaces, several principles have been followed:
Simplicity
It should be easy to learn and easy to use the module interfaces.
Safety
Interfaces should be safe, i.e. the memory integrity of the system must be guaranteed as long as only public interfaces are used; even if they are used in arbitrarily incorrect ways.
Interfaces as Contracts
Interfaces should be specified in a sufficiently precise way, using preconditions, postconditions, and sometimes equivalent source code fragments. Violations of preconditions or other programming errors should be detected and flagged as early as possible.
Portability
The services provided by public interfaces should be implementable on every modern personal computer or workstation.
Separation of User Interface and Program
User interface and program should be coupled as loosely as possible. A program should not be burdened with handling details of the user interface. It should be possible to modify user interfaces as widely as possible without changing a program's source code, and also without recompilation (the "fragile user interface problem").
Hiding Global Resources
Global resources (e.g. files, window system, menu system) should not occur in public interfaces whenever possible. The reason for that is that different platforms and emerging compound document standards (OLE, OpenDoc) sometimes have their own strategies of how to manage and arbitrate the use of global shared resources. There should be as little interference with such strategies as possible.
Picture a: Module Hierarchy of Oberon/F
The diagram above shows the module structure of Oberon/F. A small labeled rectangle denotes a module. A module which (may) import another one is placed higher in the hierarchy. A large labeled rectangle denotes a complete subsystem, consisting of several related modules.
The hierarchy is structured into a lower layer called core, and into an application layer. The core provides device drivers, the basic abstractions for storable objects, for editable objects, and for a hierarchical embedding of editable objects, i.e. for compound documents. The application layer provides three standard subsystems: a text subsystem, a subsystem for forms (e.g. dialog boxes), and a subsystem which constitutes the development environment. Miscellaneous other modules in the application layer are not shown here, e.g. various example modules.
For each core module, a short description of its functionality is given:
Module Domains defines a grouping construct for arbitrary objects. This construct is called a domain. As an example, every displayed document belongs to one domain. Most message broadcast are restricted to one domain.
Module Files provides abstract files of a hierarchical file system, and readers and writers as access paths to files.
Module Fonts handles font identification, font measurements, and font lookup.
Module Dialog provides various facilities for the interaction with the user, e.g. standard dialogs for file opening and closing, and for presenting error messages.
Module Stores defines the base type for all extensible storable objects, and file mappers for the externalization and internalization of stores.
Module Ports provides abstract display ports, riders as access paths to ports, and frames as port mappers.
Module Models defines a base type for the representation of storable data, as well as operations for the management of model modifications.
Module Views defines the central abstraction of a view, which provides presentation of a model.
Module Controllers defines the abstraction of a controllers. A controller provides for user interaction with a view and the view's model.
Module Properties defines the abstraction of properties. Properties allow to modify a view's attributes in a generic, and possibly non-interactive way.
Module Containers defines model, view, and controller for general containers, i.e. for views which may contain a variable number of arbitrary other views. This fundamental abstraction allows to hide the differences between the OLE and OpenDoc user interfaces, as far as container handling is concerned.
Module Controls gives a property interface to controls, i.e. to views such as buttons, check boxes, text entry fields, etc.
Modules which are not of general interest or do not provide portable abstractions are called private. Their interfaces may not be portable, may not be documented, or may change arbitrarily between Oberon/F releases. Most names of private modules start with the prefix "Host" or "Std", which are reserved subsystem prefixes. An implementation may contain further private global modules like Kernel, Modules, Reals, Windows, or Documents.
Private modules are not shown in the above module diagram. Some of them may be described in the host-specific documentation for a given Oberon/F implementation, but should not be considered as parts of Oberon/F proper. This holds also for interface modules with the host platform, which may be provided as well, in order to allow direct access to features particular to the given platform.
It is intended that some of the currently private modules, such as Kernel, Meta, Properties, Printers, Printing, Containers, and Documents will later be documented and become public.
An Oberon/F developer can use his own unique module name prefix for the module names of his project(s), in order to eliminate name clashes with module names of other developers. Oberon microsystems acts as a clearinghouse for such name prefixes, by providing a prefix registration service (-> "Oberon/F, User's Guide").
The module hierarchy gives an overview over the coarse structure of a software system, by showing the major abstractions and subsystems, and their import relations. On a finer level of granularity, it is helpful to consider the type hierarchies which are created through type extension. Typically, such type trees are drawn with the base type at the top, and with arrows downward to the extended types; i.e. quite unlike most natural trees, type trees have their roots at the top and grow downward. In Oberon/F, there is no need to have one common base type for all object types, thus we can draw a type forest (i.e. several type trees):
Picture b: Hierarchies of Important Dynamic Object Types
The above diagram shows all major type trees of dynamic Oberon/F objects. Interface types are drawn in boxes with thin outlines, concrete types are drawn in boxes with thicker outlines. The name of the object type, more precisely of its pointer type, is prefixed with the name of the module in which the object type is defined, e.g. Views.View denotes type View in module Views.
The hierarchy of the most important static object types are shown in the diagram below:
Picture c: Hierarchies of Important Static Object Types
In this diagram, it can be seen that most important static objects in Oberon/F are message records.
Device Drivers: Carriers, Riders, Mappers, and Directories
This chapter describes the strategy which is used for modeling device drivers, as well as data structures which can be accessed in a similar way as devices, e.g. texts.
Access to a device's data passes through two different objects: a rider and a carrier. A carrier represents a data container, e.g. a file or a screen pixelmap. Several access paths may be open on one carrier at the same time. This n:1 relationship gives rise to the separation of carriers and riders, where a rider represents one access path to its carrier.
The implementation of a rider must have intimate knowledge about the implementation of its carrier, e.g. a file rider must know about the disk sector corresponding to its current position. Thus an extension of a carrier usually requires an appropriate extension of its rider as well. As a consequence, a rider is generated by its carrier, which provides an allocation function for this purpose.
A rider provides primitive input/output operations, the so-called bottleneck interface. These primitives can be used to build more complex operations which form a higher-level abstraction, possibly even an application-specific abstraction. Such a higher-level abstraction will generally be called a mapper in this documentation. A mapper contains a rider as a link to the carrier. During their use, mappers and riders form 1:1 relations, i.e. pairs.
Picture b: Carrier-Rider-Mapper Separation
In contrast to riders, mappers know nothing about a carrier's implementation, and thus can be extended independently. This independence means that every mapper may be used on any compatible rider, without the need to implement all combinations of mappers and riders individually. This situation is shown below for the case of riders that operate on serial carriers like RS232 links or networks:
Picture c: Extensibility in two Dimensions
As was explained in chapter 1.7 ("From White-Box Frameworks towards Black-Box Frameworks"), Oberon/F generally doesn't allow the extension of concrete types. Mappers are exceptions to this rule, because by their very nature they create or interpret fixed external representations, e.g. number formats on screen or the representation of binary data on a file. Apart from possible optimizations for efficiency reasons, this fixed external representation practically defines the complete behavior of a mapper. A procedure whose complete behavior is known may safely be extended (i.e. empty procedures, default procedures, mapper procedures, procedures whose source code is published). However, mappers and message records are the only exported concrete types in Oberon/F.
Yet there must be a way to obtain objects of concrete types! For this purpose, a module which exports an interface type also provides a so-called directory object, accessible through a global variable. Such a directory provides functions to allocate new objects, and often also procedures for looking up objects by a given key (this property, although not always available, led to the generic term "directory"). For example, a file directory provides functions to create new empty files, as well as to look up existing files by name (a file directory object in Oberon/F does not, however, represent a single file subdirectory, but rather the whole file system).
Directory objects are powerful facilities, since they can be replaced at run-time (usually this is done during the boot configuration process). This makes it possible to add and integrate extended services at run-time (i.e. new concrete types), which allows a system to grow in a controlled manner.
To summarize:
- Carrier container for data
- Rider access path to carrier, bottleneck interface
- Mapper creates or interprets external data formats using a rider as link to a carrier
- Directory facility for generating or for looking up objects of concrete types
User Interaction: Models, Views, and Controllers
Oberon/F supports storable objects, i.e. objects which can be stored to and loaded from non-volatile carriers. Such objects contain arbitrary persistent state (data stored in a non-volatile way, e.g. on disk). For this purpose, a type Store is provided, which is the base type of all storable types. Stores are externalized on and internalized from files. Module Stores provides readers and writers, which are file mappers that implement a binary encoding of Oberon/L values like characters, integers, sets, or of complete stores. A store uses these mappers to read or write its internal data, which possibly may contain other stores as well, i.e. stores can be embedded recursively.
For interactive applications, three different extensions of stores are predefined by Oberon/F: models, views, and controllers. This seperation is known as the MVC paradigm, originally developed for Smalltalk at Xerox PARC (M for Model, V for View, C for Controller).
Picture a: Model-View-Controller Separation
A Model is a store that contains data which should be presented visually, e.g. a text (sequence of characters plus font attributes), or a graph (set of graphical objects plus pen attributes), etc.
A model is presented by a View. A view maps the model's contents into a rectangular display area, using frames as mappers to ports (e.g. screen ports or printer ports). Frames are also the input channels for mouse and keyboard actions.
Several views may present the same model simultaneously, e.g. there may be two views presenting different parts of the same text, or a list of numbers may be shown as a table and as a bar chart simultaneously.
Views are extended stores, and can thus be embedded in other stores, e.g. other views, or in models. The root of such a hierarchy is called a document. A document corresponds to a file, and is the root object displayed in a window.
A view may also perform user interaction. However, complex applications delegate this task to another object called a Controller. A controller may further delegate the actual operation to an operation, which is an object that specifies an invertable operation on a view or its model, i.e. it supports the undo feature.
Picture b Document Example
The example above shows the situation where two windows display a document with a text view, where the text contained in this view contains a graphic view. One of the windows is the main window, the other a child window of the same document. Child windows show all or part of a document's view hierarchy. The topmost view in a window is independent of its corresponding views in other windows, their size, scroll position, and other view attributes may differ. They only share the same model.
As a consequence, if this model contains a view, the view is shared by all views (each in its own window) displaying the same model. However, for each window there exists one frame on this shared view. Thus there may be as many frames on any view as there are windows on this document.
The outlined scheme has the effect that a document is a store hierarchy (not necessarily a tree, but a DAG, i.e. a directed acyclic graph), and that a window is a frame hierarchy (always a tree) which mirrors a subset of the store hierarchy. There are as many frame trees for a store hierarchy as there are windows displaying the same document.
To summarize:
- Store base type of all storable objects
- Model data representation
- View data presentation
- Controller interaction
- Frame user input / display output mapper
Messages
One of the most fundamental language constructs of Oberon/L are procedures. A procedure combines several statements into one more powerful entity, which can be used as a statement itself, e.g. to form even more powerful procedures.
In order to support object-oriented programming, procedures may be called indirectly via an object. This indirection leads to different actual procedures being called, depending on the object's dynamic type. Procedures which behave in such a polymorphic way are called type-bound procedures (or methods).
With any collection of similar procedures, it is theoretically possible to combine them into one procedure with an additional parameter. This parameter tells which member of the collection is selected when the procedure is called. As an example, consider the following procedures:
PROCEDURE Show
PROCEDURE Hide
These procedures can be combined into one procedure
PROCEDURE Handle (show: BOOLEAN)
This procedure internally selects a Show or Hide behavior based on the additional parameter.
Such a combination becomes problematic, however, if the parameter lists of the different procedures are different, e.g. if the above procedure should be combined with
PROCEDURE Select (from, to: LONGINT)
A solution to this problem is to pass a record as parameter to the combined procedure. Records are extensible, thus all necessary parameters can be combined in a suitable record extension:
Message = RECORD END;
ShowMsg = RECORD (Message) END;
HideMsg = RECORD (Message) END;
SelectMsg = RECORD (Message) from, to: LONGINT END;
Such a record can then be passed to a general handler procedure, e.g. a type-bound procedure as in the example below:
PROCEDURE (obj: Object) Handle (VAR msg: Message)
This procedure can not only replace the procedures
PROCEDURE (obj: Object) Show
PROCEDURE (obj: Object) Hide
PROCEDURE (obj: Object) Select (from, to: LONGINT)
but also any other procedure bound to type Object. Internally, Handle uses type tests to determine the action to be taken, and type guards to access the particular parameters (i.e. the record fields of msg).
A universal handler procedure is a simple and very general construct, but inconvenient and error-prone. The signature of a handler procedure reveals very little about what the procedure really does, and the compiler cannot tell if message record fields have not been set up correctly. Thus, Oberon/F generally uses distinct procedures for distinct operations.
However, handler procedures can be convenient under special circumstances, namely when a procedure call must be applied to a large number of objects (broadcast of a message), or when the called object lets some other object do the work for it (forwarding of a message).
In these cases, the number of normal procedures would have to be multiplied, e.g. there would have to be a BroadcastShow procedure in addition to the Show procedure, a BroadcastHide procedure in addition to the Hide procedure, etc. This is inconvenient, because it results in bloated interfaces, and because broadcast procedures typically all work exactly the same way internally, i.e. one Broadcast procedure should be sufficient in principle.
For these reasons, the concept of universal handlers has been used in Oberon/F where this was most appropriate, i.e. where broadcasting or forwarding are used extensively. In the following paragraphs, these places are introduced.
When a user types a key, presses a mouse button, or executes a menu command, these events must be communicated to the appropriate view. Such a view is called a focus view. Views are contained in documents, and documents are displayed in windows. In most user interfaces, one of these windows is the front window (i.e. the focus window), and interactions occur with this window. A window can contain a whole hierarchy of views, thus one of them must be defined as focus.
Events like the ones described above are communicated by sending appropriate controller messages to the focus view (i.e. edit messages, scroll messages, etc.). Theses messages are not sent directly to the focus view, but are passed through the whole view hierarchy until they reach the focus. This makes it possible for a view to filter out (or to perform some other modification on) a message when desired, before it is passed to one of the embedded views.
In fact, Oberon/F does not even know which view is currently the focus. Instead, it sends messages to the focus window's document, from where it is forwarded along the focus path until it reaches the focus view. Every view which receives a controller message can decide on its own whether it wants to forward the message to one of its embedded views, or whether it wants to interpret the message itself. In the latter case, it is the focus view by definition!
Controller messages are forwarded throughout a hierarchy of views, thus they are appropriate candidates for a general forward procedure and for a handler procedure as outlined earlier.
The focus view either handles a controller message itself, or it sends it to a controller object for this purpose. If the message's type is not known, it should simply be ignored, in order to allow for future extensions.
Since in Oberon/F the same document can be shown in several windows simultaneously, every view may be visible several times simultaneously. Thus it is more precise to speak about the focus frame, instead of the focus view. Accordingly, a focus message is sent along the focus window's frame tree, instead of directly along the document's view hierarchy.
Although most interactions are performed with the front window, this is not always the case. Sometimes, two windows are involved in an interaction: a dialog and a document window. As an example, the document window may contain a text view, while a "Find & Replace" dialog may lie in front of the document window. In this case, typing is routed to the dialog, while the find and replace commands operate on the document window.
To support such a situation, Oberon/F defines two foci: a target focus (in the document window) and a front focus (in the dialog box). Every controller message is sent to exactly one of them.
Picture a: Simplified Example of Focus Hierarchy
When a controller message has arrived at its focus view, it may cause some effect there. A paste message may cause the clipboard's contents to be inserted into the focus view's model, for example. Afterwards, all views which display this same model must be updated, such that they show the new situation correctly. Oberon/F realizes this change propagation by sending a so-called model message to all affected views. This is an example of a message broadcast, and thus the second place where a general message handler is used in Oberon/F, in this case a handler for model messages. Model messages are not forwarded along a specific frame path, but rather sent to all affected views, using a Broadcast procedure.
Unfortunately, there may not only be several views for the same model, but also several frames for the same view, because several windows may be open simultaneously on the same document. This requires that after a model modification, the change propagation must be performed in two steps: from the model to each affected view, and then to each affected frame. This is realized with a second broadcast mechanism, this time for so-called view messages.
Picture b: Concept of Two-Level Broadcast
All in all, there are only a few areas where universal handler procedures and extensible message records are used in Oberon/F, otherwise type-bound procedures are applied.